﻿
namespace Numbrella.Tracker
{
	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.Data.Objects.DataClasses;
	using System.Diagnostics;
	using System.Globalization;
	using System.Linq;
	using System.ServiceModel;
	using System.ServiceModel.Channels;
	using Numbrella.Interaction;
	using Numbrella.Tracker.Data;

	internal class TrackerService : ITrackerService
	{
		private static readonly object syncRoot = new object();
		private static readonly Random random = new Random();

		public ProblemDefinition GetPendingProblem()
		{
			string userName = ServiceSecurityContext.Current.PrimaryIdentity.Name;

			lock (syncRoot)
				using (TrackerEntities entities = new TrackerEntities())
				{
					User user = entities.UsersHelper.Get(userName);
					if (user == null)
					{
						Trace.TraceWarning("Received select problem request from user '{0}' which is unknown.", userName);
						return null;
					}

					// TODO: Implement valid problem selection algorithm
					// TODO: User can have more than one problem
					IList<Problem> problems = entities.ProblemsHelper.GetActive(Settings.ProblemTimeout);
					if (problems.Count == 0)
					{
						// No problems at all
						return null;
					}

					// Choose problem for user
					Problem choosed = problems[random.Next(problems.Count)];

					// Remember that user has taken problem                				
					ProblemOffer offer = ProblemOffer.CreateProblemOffer(0);
					offer.Time = DateTime.Now;
					entities.AddToProblemOffers(offer);

					offer.User = user;
					offer.Problem = choosed;

					entities.SaveChanges();

					return choosed.ToDefinition();
				}
		}

		public void AddProblem(
			string name,
			ProblemDescription description,
			ProblemImplementation implementation)
		{
			if (String.IsNullOrEmpty(name))
				throw new ArgumentNullException("name");
			if (description == null)
				throw new ArgumentNullException("description");
			if (implementation == null)
				throw new ArgumentNullException("implementation");

			string userName = ServiceSecurityContext.Current.PrimaryIdentity.Name;

			lock (syncRoot)
				using (TrackerEntities entities = new TrackerEntities())
				{
					// Find user
					User owner = entities.UsersHelper.Get(userName);
					if (owner == null)
					{
						Trace.TraceWarning("Received add problem request from user '{0}' which is unknown.", userName);
						return;
					}

					Problem problem = entities.ProblemsHelper.Get(owner, name);

					if (problem != null)
					{
						entities.DeleteObject(problem);
						entities.SaveChanges();
						Trace.TraceWarning("Problem '{0}' was re-submited by user {1}. Problem record was updated.", name, userName);
					}
					else
						Trace.TraceInformation("Problem '{0}' was submited by user {1}.", name, userName);

					// Create new problem record
					Problem newProblem = Problem.CreateProblem(0, name, description.Text, implementation.GetRawAssembly());

					// Set last notification time to current time
					newProblem.LastNotify = DateTime.Now;

					// Remember additional types
					newProblem.AdditionalTypes = new EntityCollection<AdditionalType>();
					foreach (string typeName in implementation.SerializerKnownTypeNames)
					{
						newProblem.AdditionalTypes.Add(AdditionalType.CreateAdditionalType(0, typeName));
					}

					// Add new record to database
					entities.AddToProblems(newProblem);

					// Setup problem owner
					newProblem.Owner = owner;

					// Save changes
					entities.SaveChanges();
				}
		}

		public void ReadyForSeed(string problemName, int problemHostingPort)
		{
			if (String.IsNullOrEmpty(problemName))
				throw new ArgumentNullException("problemName");
			if (problemHostingPort <= 0)
				throw new ArgumentOutOfRangeException("problemHostingPort");

			string userName = ServiceSecurityContext.Current.PrimaryIdentity.Name;

			// Extract address properties
			RemoteEndpointMessageProperty remoteEndpointProperty =
				OperationContext.Current.IncomingMessageProperties[RemoteEndpointMessageProperty.Name] as
				RemoteEndpointMessageProperty;
			if (remoteEndpointProperty == null)
			{
				Trace.TraceError("Remote IP for problem '{0}' can't be determined.", problemName);
				return;
			}

			// Build problem Uri
			string address = DetermineClientIPAddress(remoteEndpointProperty);
			Uri fullAddress = new Uri(string.Format(CultureInfo.InvariantCulture, "net.tcp://{0}:{1}/{2}", address, problemHostingPort, problemName));

			// Update problem record
			lock (syncRoot)
				using (TrackerEntities entities = new TrackerEntities())
				{
					Problem problem = entities.ProblemsHelper.Get(userName, problemName);
					if (problem == null)
					{
						Trace.TraceWarning("Received seed notification for unknown problem '{0}' of user '{1}'.", problemName, userName);
						return;
					}

					problem.Uri = fullAddress.ToString();
					problem.LastNotify = DateTime.Now;
					entities.SaveChanges();

					Trace.TraceInformation("Seed information updated for problem '{0}' of user '{1}' to '{2}'.", problem.Name, userName, fullAddress);
				}
		}

		public void DeleteProblem(string name)
		{
			if (String.IsNullOrEmpty(name))
				throw new ArgumentNullException("name");

			string userName = ServiceSecurityContext.Current.PrimaryIdentity.Name;

			// Delete problem record
			lock (syncRoot)
				using (TrackerEntities entities = new TrackerEntities())
				{
					Problem problem = entities.ProblemsHelper.Get(userName, name);

					if (problem == null)
					{
						Trace.TraceWarning("Received delete request for problem '{0}' of user '{1}' which is unknown.", name, userName);
						return;
					}

					// Delete all offers for this problem
					ReadOnlyCollection<ProblemOffer> offers = entities.OffersHelper.Get(problem);
					foreach (ProblemOffer offer in offers)
					{
						entities.DeleteObject(offer);
					}

					entities.DeleteObject(problem);
					entities.SaveChanges();

					Trace.TraceInformation("Problem '{0}' of user '{1}' have been deleted.", problem.Name, userName);
				}
		}

		public void NotifyAboutTaskCompletion(string problemName, int taskID, IEnumerable<string> userNames)
		{
			if (userNames == null)
				throw new ArgumentNullException("userNames");
			if (String.IsNullOrEmpty(problemName))
				throw new ArgumentNullException("problemName");
			if (taskID < 0)
				throw new ArgumentOutOfRangeException("taskID");

			string authorName = ServiceSecurityContext.Current.PrimaryIdentity.Name;

			lock (syncRoot)
				using (TrackerEntities entities = new TrackerEntities())
				{
					// Find problem
					Problem problem = entities.ProblemsHelper.Get(authorName, problemName);
					if (problem == null)
					{
						Trace.TraceWarning("Received task solved notification for problem '{0}' of user '{1}' which is unknown.", problemName, authorName);
						return;
					}

					foreach (string userName in userNames)
					{
						// Find solver
						User solver = entities.UsersHelper.Get(userName);
						if (solver == null)
						{
							Trace.TraceWarning("Received task solve for problem '{0}' with solverName='{1}' which is unknown.", problem.Name, userName);
							continue;
						}

						// Check if solver has taken that problem	
						// TODO: We need to provide offer id to tracker, and check it here
						ReadOnlyCollection<ProblemOffer> offers = entities.OffersHelper.Get(solver, problem);
						if (offers.Count == 0)
						{
							Trace.TraceWarning(
								"The task solve notification for problem '{0}' about the user '{1}' who did not take this problem is received.",
								problem.Name,
								solver.Name);
							continue;
						}

						// TODO: Remove offer with given id
						entities.DeleteObject(offers.First());
						entities.SaveChanges();

						Trace.TraceInformation("User '{0}' solved task {1} of problem '{2}'.", solver.Name, taskID, problem.Name);
					}
				}
		}

		private static string DetermineClientIPAddress(RemoteEndpointMessageProperty remoteEndpointProperty)
		{
			string addressString = remoteEndpointProperty.Address;
			UriHostNameType hostNameType = Uri.CheckHostName(addressString);
			switch (hostNameType)
			{
				case UriHostNameType.IPv4:
					return addressString;
				case UriHostNameType.IPv6:
					return String.Format(CultureInfo.InvariantCulture, "[{0}]", addressString);
				default:
					throw new InvalidOperationException("Determined client IP address is neither IPv4 nor IPv6.");
			}
		}
	}
}